Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ

Node / meshtastic / Meshtastic-Android / files / feature / firmware / src / commonTest / kotlin / org / meshtastic / feature / firmware / ota / dfu / LegacyDfuTransportTest.kt

Displaying Raw β€’ Download

feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransportTest.kt bd2863243bab6eb213401d949839a2bc74dde7e2 (bd286324) Text, 46.34 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tf0883e@fileTb4b4b4:Te6edf3SuppressTb4b4b4(Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffLargeClassTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTooManyFunctionsTa5d6ff"Tb4b4b4)

Tff7b72package T7ee787org.meshtastic.feature.firmware.ota.dfu

Tff7b72import T7ee787kotlinx.coroutines.CancellationException
Tff7b72import T7ee787kotlinx.coroutines.CompletableDeferred
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.Dispatchers
Tff7b72import T7ee787kotlinx.coroutines.ExperimentalCoroutinesApi
Tff7b72import T7ee787kotlinx.coroutines.Job
Tff7b72import T7ee787kotlinx.coroutines.async
Tff7b72import T7ee787kotlinx.coroutines.awaitCancellation
Tff7b72import T7ee787kotlinx.coroutines.flow.Flow
Tff7b72import T7ee787kotlinx.coroutines.flow.FlowCollector
Tff7b72import T7ee787kotlinx.coroutines.flow.StateFlow
Tff7b72import T7ee787kotlinx.coroutines.test.advanceUntilIdle
Tff7b72import T7ee787kotlinx.coroutines.test.runCurrent
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787org.meshtastic.core.ble.BleCharacteristic
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnection
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnectionFactory
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnectionState
Tff7b72import T7ee787org.meshtastic.core.ble.BleDevice
Tff7b72import T7ee787org.meshtastic.core.ble.BleService
Tff7b72import T7ee787org.meshtastic.core.ble.BleWriteType
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleConnection
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleConnectionFactory
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleDevice
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleScanner
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleService
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleWrite
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertContentEquals
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFailsWith
Tff7b72import T7ee787kotlin.test.assertIs
Tff7b72import T7ee787kotlin.test.assertSame
Tff7b72import T7ee787kotlin.test.assertTrue
Tff7b72import T7ee787kotlin.time.Duration

Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72class T56d364LegacyDfuTransportTest Tb4b4b4{

Tff7b72private Tff7b72val Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ff00:11:22:33:44:55Ta5d6ff"
Tff7b72private Tff7b72val Te6edf3dfuAddress Tff7b72= Ta5d6ff"Ta5d6ff00:11:22:33:44:56Ta5d6ff"

T8b949e// -----------------------------------------------------------------------
T8b949e// Phase 2: connectToDfuMode
T8b949e// -----------------------------------------------------------------------

Tf0883e@Test
Tff7b72fun Td2a8ff`connectToDfuMode succeeds when bootloader exposes 1530 service`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3transport Tff7b72=
Te6edf3LegacyDfuTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3FakeBleConnectionFactoryTb4b4b4(Te6edf3connectionTb4b4b4)Tb4b4b4, Te6edf3addressTb4b4b4, Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4)

Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3dfuAddressTb4b4b4)Tb4b4b4)

Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3connectToDfuModeTb4b4b4(Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6ffconnectToDfuMode failed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`connectToDfuMode fails fast on unsupported old DFU Version lt 5`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
T8b949e// Pre-seed the version characteristic with a SDK 6 version (0x0004 LE).
Te6edf3connectionTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3enqueueReadTb4b4b4(Te6edf3LEGACY_DFU_VERSION_UUIDTb4b4b4, Te6edf3byteArrayOfTb4b4b4(T79c0ff0Te6edf3x04Tb4b4b4, T79c0ff0Te6edf3x00Tb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3transport Tff7b72=
Te6edf3LegacyDfuTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3FakeBleConnectionFactoryTb4b4b4(Te6edf3connectionTb4b4b4)Tb4b4b4, Te6edf3addressTb4b4b4, Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4)

Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3dfuAddressTb4b4b4)Tb4b4b4)

Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3connectToDfuModeTb4b4b4(Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3UnsupportedBootloaderTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`connectToDfuMode accepts modern DFU Version 8`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Te6edf3connectionTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3enqueueReadTb4b4b4(Te6edf3LEGACY_DFU_VERSION_UUIDTb4b4b4, Te6edf3byteArrayOfTb4b4b4(T79c0ff0Te6edf3x08Tb4b4b4, T79c0ff0Te6edf3x00Tb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3transport Tff7b72=
Te6edf3LegacyDfuTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3FakeBleConnectionFactoryTb4b4b4(Te6edf3connectionTb4b4b4)Tb4b4b4, Te6edf3addressTb4b4b4, Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4)

Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3dfuAddressTb4b4b4)Tb4b4b4)

Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3connectToDfuModeTb4b4b4(Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`connectToDfuMode accepts missing DFU Version characteristic`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
T8b949e// Default FakeBleService.read returns empty bytes when nothing is enqueued β€” treated as "absent".
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3transport Tff7b72=
Te6edf3LegacyDfuTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3FakeBleConnectionFactoryTb4b4b4(Te6edf3connectionTb4b4b4)Tb4b4b4, Te6edf3addressTb4b4b4, Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4)

Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3dfuAddressTb4b4b4)Tb4b4b4)

Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3connectToDfuModeTb4b4b4(Tb4b4b4)

Te6edf3assertTrueTb4b4b4(
Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4,
Ta5d6ff"Ta5d6ffMissing DFU Version should be treated as modern (proceed): Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

T8b949e// -----------------------------------------------------------------------
T8b949e// Phase 3: transferInitPacket preflight
T8b949e// -----------------------------------------------------------------------

Tf0883e@Test
Tff7b72fun Td2a8ff`transferInitPacket rejects oversized init packet looks like Secure-shaped dat`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3transport Tff7b72=
Te6edf3LegacyDfuTransportTb4b4b4(
Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3FakeBleConnectionFactoryTb4b4b4(Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3addressTb4b4b4,
Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Tb4b4b4)
Tff7b72val Te6edf3oversized Tff7b72= Te6edf3ByteArrayTb4b4b4(Te6edf3LegacyDfuTransportTb4b4b4.Te6edf3MAX_REASONABLE_LEGACY_INIT_SIZE Tff7b72+ T79c0ff1Tb4b4b4) Tb4b4b4{ T79c0ff0Te6edf3x42 Tb4b4b4}

Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3oversizedTb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3InitPacketNotLegacyTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`transferInitPacket accepts typical 14 byte legacy init`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3transport Tff7b72=
Te6edf3LegacyDfuTransportTb4b4b4(
Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3FakeBleConnectionFactoryTb4b4b4(Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3addressTb4b4b4,
Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Tb4b4b4)
Tff7b72val Te6edf3init Tff7b72= Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}

Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Tff7b72initTb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4)
Tb4b4b4}

T8b949e// -----------------------------------------------------------------------
T8b949e// Phase 4: transferFirmware happy path
T8b949e// -----------------------------------------------------------------------

Tf0883e@Test
Tff7b72fun Td2a8ff`transferFirmware happy path writes correct opcode and packet sequence`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath

Tff7b72val Te6edf3init Tff7b72= Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3firmware Tff7b72= Te6edf3ByteArrayTb4b4b4(T79c0ff8T79c0ff0Tb4b4b4) Tb4b4b4{ Tb4b4b4(T79c0ff0Te6edf3xA0 Tff7b72+ Tffa657itTb4b4b4)Tb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4} T8b949e// 4 packets at MTU=20

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Tff7b72initTb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3progress Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657FloatTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3firmwareTb4b4b4) Tb4b4b4{ Te6edf3progressTb4b4b4.Te6edf3addTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6fftransferFirmware failed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)

T8b949e// Control-point opcode order:
T8b949e// START_DFU, INIT_DFU_PARAMS_START, INIT_DFU_PARAMS_COMPLETE,
T8b949e// PACKET_RECEIPT_NOTIF_REQ, RECEIVE_FIRMWARE_IMAGE, VALIDATE, ACTIVATE_AND_RESET
Tff7b72val Te6edf3cpOpcodes Tff7b72= Te6edf3envTb4b4b4.Te6edf3controlPointWritesTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTff7b72[T79c0ff0Tff7b72] Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(
Te6edf3listOfTb4b4b4(
Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3START_DFUTb4b4b4,
Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3INIT_DFU_PARAMSTb4b4b4,
Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3INIT_DFU_PARAMSTb4b4b4,
Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3PACKET_RECEIPT_NOTIF_REQTb4b4b4,
Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RECEIVE_FIRMWARE_IMAGETb4b4b4,
Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3VALIDATETb4b4b4,
Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3ACTIVATE_AND_RESETTb4b4b4,
Tb4b4b4)Tb4b4b4,
Te6edf3cpOpcodesTb4b4b4,
Tb4b4b4)

T8b949e// First INIT_DFU_PARAMS sub-opcode is START (0x00), second is COMPLETE (0x01).
Tff7b72val Te6edf3initParamsWrites Tff7b72= Te6edf3envTb4b4b4.Te6edf3controlPointWritesTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTff7b72[T79c0ff0Tff7b72] Tff7b72=Tff7b72= Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3INIT_DFU_PARAMS Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3INIT_PARAMS_STARTTb4b4b4, Te6edf3initParamsWritesTff7b72[T79c0ff0Tff7b72]Tb4b4b4.Te6edf3dataTff7b72[T79c0ff1Tff7b72]Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3INIT_PARAMS_COMPLETETb4b4b4, Te6edf3initParamsWritesTff7b72[T79c0ff1Tff7b72]Tb4b4b4.Te6edf3dataTff7b72[T79c0ff1Tff7b72]Tb4b4b4)

T8b949e// START_DFU includes APP image type byte.
Tff7b72val Te6edf3startWrite Tff7b72= Te6edf3envTb4b4b4.Te6edf3controlPointWritesTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3single Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTff7b72[T79c0ff0Tff7b72] Tff7b72=Tff7b72= Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3START_DFU Tb4b4b4}
Te6edf3assertContentEqualsTb4b4b4(Te6edf3byteArrayOfTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3START_DFUTb4b4b4, Te6edf3LegacyDfuImageTypeTb4b4b4.Te6edf3APPLICATIONTb4b4b4)Tb4b4b4, Te6edf3startWriteTb4b4b4.Te6edf3dataTb4b4b4)

T8b949e// Packet writes should contain: [12B image sizes] then [14B init in chunks] then [firmware in chunks].
Tff7b72val Te6edf3packetBytes Tff7b72= Te6edf3envTb4b4b4.Te6edf3packetWritesTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3flatMap Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3toListTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4.Te6edf3toByteArrayTb4b4b4(Tb4b4b4)
T8b949e// Image sizes payload (first 12 bytes): app size = firmware.size, others 0.
Tff7b72val Te6edf3imageSizes Tff7b72= Te6edf3packetBytesTb4b4b4.Te6edf3copyOfRangeTb4b4b4(T79c0ff0Tb4b4b4, T79c0ff1T79c0ff2Tb4b4b4)
Te6edf3assertContentEqualsTb4b4b4(Te6edf3legacyImageSizesPayloadTb4b4b4(Te6edf3appSize Tff7b72= Te6edf3firmwareTb4b4b4.Te6edf3sizeTb4b4b4)Tb4b4b4, Te6edf3imageSizesTb4b4b4)
T8b949e// Init follows.
Te6edf3assertContentEqualsTb4b4b4(Tff7b72initTb4b4b4, Te6edf3packetBytesTb4b4b4.Te6edf3copyOfRangeTb4b4b4(T79c0ff1T79c0ff2Tb4b4b4, T79c0ff1T79c0ff2 Tff7b72+ Tff7b72initTb4b4b4.Te6edf3sizeTb4b4b4)Tb4b4b4)
T8b949e// Firmware follows.
Te6edf3assertContentEqualsTb4b4b4(Te6edf3firmwareTb4b4b4, Te6edf3packetBytesTb4b4b4.Te6edf3copyOfRangeTb4b4b4(T79c0ff1T79c0ff2 Tff7b72+ Tff7b72initTb4b4b4.Te6edf3sizeTb4b4b4, Te6edf3packetBytesTb4b4b4.Te6edf3sizeTb4b4b4)Tb4b4b4)

T8b949e// Final progress should be 1.0.
Te6edf3assertEqualsTb4b4b4(T79c0ff1fTb4b4b4, Te6edf3progressTb4b4b4.Te6edf3lastTb4b4b4(Tb4b4b4)Tb4b4b4)

T8b949e// Packet writes use WITHOUT_RESPONSE.
Te6edf3envTb4b4b4.Te6edf3packetWritesTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3assertEqualsTb4b4b4(Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITHOUT_RESPONSETb4b4b4, Tffa657itTb4b4b4.Te6edf3writeTypeTb4b4b4) Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`transferFirmware uses high-MTU packets when the link negotiates a larger MTU`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Te6edf3mtu Tff7b72= T79c0ff2T79c0ff4T79c0ff4Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff6T79c0ff0T79c0ff0Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4) Tb4b4b4{Tb4b4b4}

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6fftransferFirmware failed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
T8b949e// Firmware streams in 244-byte packets (negotiated MTU βˆ’ 3) instead of the 20-byte default β€” the ~12Γ— win.
Te6edf3assertEqualsTb4b4b4(T79c0ff2T79c0ff4T79c0ff4Tb4b4b4, Te6edf3envTb4b4b4.Te6edf3packetWritesTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3maxOf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3size Tb4b4b4}Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`transferFirmware floors the packet size to a 4-byte boundary`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Te6edf3mtu Tff7b72= T79c0ff2T79c0ff4T79c0ff2Tb4b4b4) T8b949e// not a multiple of 4
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff6T79c0ff0T79c0ff0Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4) Tb4b4b4{Tb4b4b4}

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6fftransferFirmware failed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
T8b949e// 242 β†’ 240: the Adafruit DFU data path rejects writes whose length isn't a multiple of 4.
Te6edf3assertEqualsTb4b4b4(T79c0ff2T79c0ff4T79c0ff0Tb4b4b4, Te6edf3envTb4b4b4.Te6edf3packetWritesTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3maxOf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3size Tb4b4b4}Tb4b4b4)
Tb4b4b4}

T8b949e// -----------------------------------------------------------------------
T8b949e// Phase 4: error paths
T8b949e// -----------------------------------------------------------------------

Tf0883e@Test
Tff7b72fun Td2a8ff`transferFirmware fails with ProtocolError when device rejects START_DFU`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3RejectStart

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff4T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4}

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Tff7b72val Te6edf3ex Tff7b72= Te6edf3assertIsTff7b72<Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3ProtocolErrorTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3START_DFUTb4b4b4, Te6edf3exTb4b4b4.Te6edf3requestOpcodeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuStatusTb4b4b4.Te6edf3NOT_SUPPORTEDTb4b4b4, Te6edf3exTb4b4b4.Te6edf3statusTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`transferFirmware maps START_DFU INVALID_STATE to StaleSessionReset for recovery restart`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3RejectStartInvalidState

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff4T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4}

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3StaleSessionResetTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`transferFirmware fails with ProtocolError when device rejects INIT_DFU_PARAMS`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3RejectInit

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff4T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4}

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Tff7b72val Te6edf3ex Tff7b72= Te6edf3assertIsTff7b72<Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3ProtocolErrorTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3INIT_DFU_PARAMSTb4b4b4, Te6edf3exTb4b4b4.Te6edf3requestOpcodeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuStatusTb4b4b4.Te6edf3OPERATION_FAILEDTb4b4b4, Te6edf3exTb4b4b4.Te6edf3statusTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`transferFirmware fails with ProtocolError when device rejects VALIDATE`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3RejectValidate

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff4T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4}

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Tff7b72val Te6edf3ex Tff7b72= Te6edf3assertIsTff7b72<Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3ProtocolErrorTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3VALIDATETb4b4b4, Te6edf3exTb4b4b4.Te6edf3requestOpcodeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuStatusTb4b4b4.Te6edf3CRC_ERRORTb4b4b4, Te6edf3exTb4b4b4.Te6edf3statusTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`transferFirmware fails with PacketReceiptMismatch when device under-reports`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3PrnUnderReport

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
T8b949e// Send (PRN_INTERVAL_PACKETS + 1) packets of 20 bytes β€” guarantees a PRN window fires
T8b949e// before the firmware completes, so the under-reported byte count surfaces as a mismatch.
Tff7b72val Te6edf3firmwareSize Tff7b72= Tb4b4b4(Te6edf3PRN_INTERVAL_PACKETS Tff7b72+ T79c0ff1Tb4b4b4) Tff7b72* T79c0ff2T79c0ff0
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(Te6edf3firmwareSizeTb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4}

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3PacketReceiptMismatchTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`transferFirmware tolerates ACTIVATE_AND_RESET write failure - disconnect race`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath
T8b949e// After VALIDATE response is sent, ACTIVATE write should be treated as success even if the device throws.
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3failOnActivateWrite Tff7b72= Tff7b72true

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff4T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4}

Te6edf3assertTrueTb4b4b4(
Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4,
Ta5d6ff"Ta5d6ffACTIVATE write failure must be treated as success; got: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

T8b949e// -----------------------------------------------------------------------
T8b949e// Abort
T8b949e// -----------------------------------------------------------------------

Tf0883e@Test
Tff7b72fun Td2a8ff`abort writes RESET opcode through control point`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3transport Tff7b72=
Te6edf3LegacyDfuTransportTb4b4b4(Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3FakeBleConnectionFactoryTb4b4b4(Te6edf3connectionTb4b4b4)Tb4b4b4, Te6edf3addressTb4b4b4, Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4)

Te6edf3transportTb4b4b4.Te6edf3abortTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3write Tff7b72= Te6edf3connectionTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3writesTb4b4b4.Te6edf3singleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuUuidsTb4b4b4.Te6edf3CONTROL_POINTTb4b4b4, Te6edf3writeTb4b4b4.Te6edf3characteristicTb4b4b4.Te6edf3uuidTb4b4b4)
Te6edf3assertContentEqualsTb4b4b4(Te6edf3byteArrayOfTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RESETTb4b4b4)Tb4b4b4, Te6edf3writeTb4b4b4.Te6edf3dataTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITH_RESPONSETb4b4b4, Te6edf3writeTb4b4b4.Te6edf3writeTypeTb4b4b4)
Tb4b4b4}

T8b949e// -----------------------------------------------------------------------
T8b949e// Test infrastructure
T8b949e// -----------------------------------------------------------------------

Tff7b72private Tff7b72class T56d364TestEnvTb4b4b4(
Tff7b72val Te6edf3transportTb4b4b4: Te6edf3LegacyDfuTransportTb4b4b4,
Tff7b72val Te6edf3serviceTb4b4b4: Te6edf3AutoRespondingLegacyServiceTb4b4b4,
Tff7b72val Te6edf3connectionTb4b4b4: Te6edf3FakeBleConnectionTb4b4b4,
Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3responder Tff7b72= Te6edf3serviceTb4b4b4.Te6edf3responder

Tff7b72fun Td2a8ffcontrolPointWritesTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3FakeBleWriteTff7b72> Tff7b72=
Te6edf3serviceTb4b4b4.Te6edf3delegateTb4b4b4.Te6edf3writesTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3characteristicTb4b4b4.Te6edf3uuid Tff7b72=Tff7b72= Te6edf3LegacyDfuUuidsTb4b4b4.Te6edf3CONTROL_POINT Tb4b4b4}

Tff7b72fun Td2a8ffpacketWritesTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3FakeBleWriteTff7b72> Tff7b72=
Te6edf3serviceTb4b4b4.Te6edf3delegateTb4b4b4.Te6edf3writesTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3characteristicTb4b4b4.Te6edf3uuid Tff7b72=Tff7b72= Te6edf3LEGACY_DFU_PACKET_UUID Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffcreateConnectedTransportTb4b4b4(
Te6edf3mtuTb4b4b4: Tffa657Int? Tff7b72= Tff7b72nullTb4b4b4,
Te6edf3profileTb4b4b4: Te6edf3LegacyDfuStreamProfile Tff7b72= Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3NORMALTb4b4b4,
Te6edf3suppressDisconnectEmissionsTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72falseTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3TestEnv Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3fakeConnection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{ Te6edf3maxWriteValueLength Tff7b72= Te6edf3mtu Tb4b4b4}
Tff7b72val Te6edf3autoService Tff7b72= Te6edf3AutoRespondingLegacyServiceTb4b4b4(Te6edf3fakeConnectionTb4b4b4.Te6edf3serviceTb4b4b4)
Tff7b72val Te6edf3factory Tff7b72=
Tff7b72object Tb4b4b4: T56d364BleConnectionFactory Tb4b4b4{
Tff7b72override Tff7b72fun Td2a8ffcreateTb4b4b4(Te6edf3scopeTb4b4b4: Te6edf3CoroutineScopeTb4b4b4, Te6edf3tagTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Te6edf3BleConnection Tff7b72= Te6edf3AutoRespondingBleConnectionTb4b4b4(
Te6edf3delegate Tff7b72= Te6edf3fakeConnectionTb4b4b4,
Te6edf3autoService Tff7b72= Te6edf3autoServiceTb4b4b4,
Te6edf3profileScope Tff7b72= Te6edf3scopeTb4b4b4,
Te6edf3suppressDisconnectEmissions Tff7b72= Te6edf3suppressDisconnectEmissionsTb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tff7b72val Te6edf3transport Tff7b72= Te6edf3LegacyDfuTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3factoryTb4b4b4, Te6edf3addressTb4b4b4, Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4, Te6edf3profileTb4b4b4)

Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3dfuAddressTb4b4b4)Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3connectToDfuModeTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3TestEnvTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3autoServiceTb4b4b4, Te6edf3fakeConnectionTb4b4b4)
Tb4b4b4}

T8b949e/**
* Drives the simulated bootloader response stream. After each `write()` to control point or packet, this service
* synthesises the appropriate notification(s) on the control-point characteristic so the transport's pending
* `awaitResponse` / `awaitPacketReceiptDuringStream` calls unblock.
*/
Tff7b72private Tff7b72class T56d364AutoRespondingLegacyServiceTb4b4b4(Tff7b72val Te6edf3delegateTb4b4b4: Te6edf3FakeBleServiceTb4b4b4) Tb4b4b4: Te6edf3BleService Tb4b4b4{
Tff7b72val Te6edf3responder Tff7b72= Te6edf3LegacyResponderTb4b4b4(Tb4b4b4)

T8b949e/**
* When `true`, the next `write()` to the Legacy Control Point characteristic suspends forever (via
* [awaitCancellation]) instead of completing. Used to prove the bounded abort returns within
* [LegacyDfuTransport.RESET_WRITE_TIMEOUT] when the device never ACKs the RESET.
*/
Tff7b72var Te6edf3hangOnControlPointWritesTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72false

T8b949e/**
* When `true`, the next `write()` to the Legacy Control Point characteristic throws an [AssertionError]. Used
* to prove [LegacyDfuTransport.abort] (which catches [Exception], not [Throwable]) does not swallow [Error]
* subtypes.
*/
Tff7b72var Te6edf3throwErrorOnControlPointWritesTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72false

T8b949e/**
* When `true`, the next `write()` to the Legacy Control Point characteristic throws a [RuntimeException]. Used
* to prove [LegacyDfuTransport.abort] swallows operational [Exception] subtypes (best-effort / non-fatal).
*/
Tff7b72var Te6edf3throwExceptionOnControlPointWritesTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72false

Tff7b72override Tff7b72fun Td2a8ffhasCharacteristicTb4b4b4(Te6edf3cTb4b4b4: Te6edf3BleCharacteristicTb4b4b4) Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3hasCharacteristicTb4b4b4(Te6edf3cTb4b4b4)

Tff7b72override Tff7b72fun Td2a8ffobserveTb4b4b4(Te6edf3cTb4b4b4: Te6edf3BleCharacteristicTb4b4b4)Tb4b4b4: Te6edf3FlowTff7b72<Te6edf3ByteArrayTff7b72> Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3observeTb4b4b4(Te6edf3cTb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffreadTb4b4b4(Te6edf3cTb4b4b4: Te6edf3BleCharacteristicTb4b4b4)Tb4b4b4: Te6edf3ByteArray Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3readTb4b4b4(Te6edf3cTb4b4b4)

Tff7b72override Tff7b72fun Td2a8ffpreferredWriteTypeTb4b4b4(Te6edf3cTb4b4b4: Te6edf3BleCharacteristicTb4b4b4)Tb4b4b4: Te6edf3BleWriteType Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3preferredWriteTypeTb4b4b4(Te6edf3cTb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffwriteTb4b4b4(Te6edf3cTb4b4b4: Te6edf3BleCharacteristicTb4b4b4, Te6edf3dataTb4b4b4: Te6edf3ByteArrayTb4b4b4, Te6edf3writeTypeTb4b4b4: Te6edf3BleWriteTypeTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3throwErrorOnControlPointWrites Tff7b72&Tff7b72& Te6edf3cTb4b4b4.Te6edf3uuid Tff7b72=Tff7b72= Te6edf3LegacyDfuUuidsTb4b4b4.Te6edf3CONTROL_POINTTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3AssertionErrorTb4b4b4(Ta5d6ff"Ta5d6ffSimulated assertion failure during control point writeTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3throwExceptionOnControlPointWrites Tff7b72&Tff7b72& Te6edf3cTb4b4b4.Te6edf3uuid Tff7b72=Tff7b72= Te6edf3LegacyDfuUuidsTb4b4b4.Te6edf3CONTROL_POINTTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3RuntimeExceptionTb4b4b4(Ta5d6ff"Ta5d6ffSimulated link failure during control point writeTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3hangOnControlPointWrites Tff7b72&Tff7b72& Te6edf3cTb4b4b4.Te6edf3uuid Tff7b72=Tff7b72= Te6edf3LegacyDfuUuidsTb4b4b4.Te6edf3CONTROL_POINTTb4b4b4) Tb4b4b4{
Te6edf3awaitCancellationTb4b4b4(Tb4b4b4)
Tb4b4b4}
Te6edf3delegateTb4b4b4.Te6edf3writeTb4b4b4(Te6edf3cTb4b4b4, Te6edf3dataTb4b4b4, Te6edf3writeTypeTb4b4b4)
Tff7b72val Te6edf3response Tff7b72= Te6edf3responderTb4b4b4.Te6edf3onWriteTb4b4b4(Te6edf3cTb4b4b4.Te6edf3uuidTb4b4b4, Te6edf3dataTb4b4b4) Tff7b72?: Tff7b72return
Te6edf3responseTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3delegateTb4b4b4.Te6edf3emitNotificationTb4b4b4(Te6edf3LegacyDfuUuidsTb4b4b4.Te6edf3CONTROL_POINTTb4b4b4, Tffa657itTb4b4b4) Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/** What the simulated bootloader is meant to do for this test case. */
Tff7b72enum Tff7b72class T56d364LegacyResponderScheme Tb4b4b4{
Te6edf3HappyPathTb4b4b4,
Te6edf3RejectStartTb4b4b4,
Te6edf3RejectStartInvalidStateTb4b4b4,
Te6edf3RejectInitTb4b4b4,
Te6edf3RejectValidateTb4b4b4,
Te6edf3PrnUnderReportTb4b4b4,
Tb4b4b4}

T8b949e/**
* Synthesises Legacy DFU notifications based on the transport's current write. Behaviour depends on [scheme]:
* - `HappyPath`: returns Success for every control-point opcode that expects a response, plus accurate PRN receipts
* as packets accumulate.
* - Reject* variants: return Failure with the indicated status for the targeted opcode.
* - `PrnUnderReport`: at the first PRN window, report bytesReceived = actual βˆ’ 1 to trigger PacketReceiptMismatch.
*
* Image-sizes write (12B on packet) is the trigger for the START_DFU response β€” matching the real protocol where
* the device responds *after* it sees both the opcode and the size payload.
*/
Tff7b72class T56d364LegacyResponder Tb4b4b4{
Tff7b72var Te6edf3schemeTb4b4b4: Te6edf3LegacyResponderScheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath
Tff7b72var Te6edf3failOnActivateWriteTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72false

T8b949e/**
* Optional suspend hook invoked after each firmware-packet write with the cumulative byte count, while
* [AutoRespondingLegacyService.write] is still in progress. Tests use this to hold a write in flight (e.g. via
* [awaitCancellation]) while another coroutine changes connection state to simulate a mid-stream link drop at a
* deterministic offset.
*/
Tff7b72var Te6edf3onFirmwarePacketWriteTb4b4b4: Tb4b4b4(Te6edf3suspend Tb4b4b4(Te6edf3bytesReceivedTb4b4b4: Tffa657LongTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4)Tff7b72? Tff7b72= Tff7b72null

T8b949e/**
* When `true`, packet-receipt notifications are NOT emitted by the simulated bootloader. Used to suspend the
* transport inside the PRN wait so a test can drive a disconnect while the wait is in flight.
*/
Tff7b72var Te6edf3suppressPrnNotificationsTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72false

Tff7b72private Tff7b72var Te6edf3packetBytesReceived Tff7b72= T79c0ff0L
Tff7b72private Tff7b72var Te6edf3packetsSinceLastPrn Tff7b72= T79c0ff0
Tff7b72private Tff7b72var Te6edf3firmwareTransferStarted Tff7b72= Tff7b72false
Tff7b72private Tff7b72var Te6edf3imageSizesWritten Tff7b72= Tff7b72false
Tff7b72private Tff7b72var Te6edf3expectedFirmwareSizeTb4b4b4: Tffa657Int Tff7b72= T79c0ff0

T8b949e/**
* Effective PRN cadence. Defaults to the NORMAL profile value; updated from the `PACKET_RECEIPT_NOTIF_REQ`
* control write so the responder matches whatever interval the transport requested (NORMAL=10, RECOVERY=5).
*/
Tff7b72private Tff7b72var Te6edf3prnIntervalTb4b4b4: Tffa657Int Tff7b72= Te6edf3PRN_INTERVAL_PACKETS

Tff7b72suspend Tff7b72fun Td2a8ffonWriteTb4b4b4(Te6edf3uuidTb4b4b4: Te6edf3kotlinTb4b4b4.Te6edf3uuidTb4b4b4.Te6edf3UuidTb4b4b4, Te6edf3dataTb4b4b4: Te6edf3ByteArrayTb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3ByteArrayTff7b72>Tff7b72? Tff7b72= Tff7b72when Tb4b4b4(Te6edf3uuidTb4b4b4) Tb4b4b4{
Te6edf3LegacyDfuUuidsTb4b4b4.Te6edf3CONTROL_POINT Tff7b72-Tff7b72> Te6edf3handleControlWriteTb4b4b4(Te6edf3dataTb4b4b4)
Te6edf3LEGACY_DFU_PACKET_UUID Tff7b72-Tff7b72> Te6edf3handlePacketWriteTb4b4b4(Te6edf3dataTb4b4b4)
Tff7b72else Tff7b72-Tff7b72> Tff7b72null
Tb4b4b4}

Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffReturnCountTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffhandleControlWriteTb4b4b4(Te6edf3dataTb4b4b4: Te6edf3ByteArrayTb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3ByteArrayTff7b72>Tff7b72? Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3dataTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4) Tff7b72return Tff7b72null
Tff7b72val Te6edf3opcode Tff7b72= Te6edf3dataTff7b72[T79c0ff0Tff7b72]
Tff7b72return Tff7b72when Tb4b4b4(Te6edf3opcodeTb4b4b4) Tb4b4b4{
Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3START_DFU Tff7b72-Tff7b72> Tff7b72null

T8b949e// response comes after image sizes packet write
Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3INIT_DFU_PARAMS Tff7b72-Tff7b72> Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3dataTb4b4b4.Te6edf3size Tff7b72>Tff7b72= T79c0ff2 Tff7b72&Tff7b72& Te6edf3dataTff7b72[T79c0ff1Tff7b72] Tff7b72=Tff7b72= Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3INIT_PARAMS_COMPLETETb4b4b4) Tb4b4b4{
Te6edf3listOfTb4b4b4(Te6edf3initResponseTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Tff7b72null
Tb4b4b4}
Tb4b4b4}

Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3PACKET_RECEIPT_NOTIF_REQ Tff7b72-Tff7b72> Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3dataTb4b4b4.Te6edf3size Tff7b72>Tff7b72= T79c0ff3Tb4b4b4) Tb4b4b4{
T8b949e// Parse uint16-LE PRN value the transport just wrote and follow it for receipt cadence.
Te6edf3prnInterval Tff7b72= Tb4b4b4(Te6edf3dataTff7b72[T79c0ff1Tff7b72]Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4) Te6edf3or Tb4b4b4(Tb4b4b4(Te6edf3dataTff7b72[T79c0ff2Tff7b72]Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4) Te6edf3shl T79c0ff8Tb4b4b4)
Tb4b4b4}
Tff7b72null
Tb4b4b4}

Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RECEIVE_FIRMWARE_IMAGE Tff7b72-Tff7b72> Tb4b4b4{
Te6edf3firmwareTransferStarted Tff7b72= Tff7b72true
Tff7b72null
Tb4b4b4}

Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3VALIDATE Tff7b72-Tff7b72> Te6edf3listOfTb4b4b4(Te6edf3validateResponseTb4b4b4(Tb4b4b4)Tb4b4b4)

Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3ACTIVATE_AND_RESET Tff7b72-Tff7b72> Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3failOnActivateWriteTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3RuntimeExceptionTb4b4b4(Ta5d6ff"Ta5d6ffSimulated disconnect during ACTIVATE writeTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tff7b72null
Tb4b4b4}

Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RESET Tff7b72-Tff7b72> Tff7b72null

Tff7b72else Tff7b72-Tff7b72> Tff7b72null
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffhandlePacketWriteTb4b4b4(Te6edf3dataTb4b4b4: Te6edf3ByteArrayTb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3ByteArrayTff7b72>Tff7b72? Tb4b4b4{
T8b949e// First packet write is the 12-byte image sizes payload (after START_DFU).
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3imageSizesWrittenTb4b4b4) Tb4b4b4{
Te6edf3imageSizesWritten Tff7b72= Tff7b72true
T8b949e// Parse appSize from bytes 8..11 (LE u32).
Tff7b72if Tb4b4b4(Te6edf3dataTb4b4b4.Te6edf3size Tff7b72>Tff7b72= T79c0ff1T79c0ff2Tb4b4b4) Tb4b4b4{
Te6edf3expectedFirmwareSize Tff7b72=
Tb4b4b4(Te6edf3dataTff7b72[T79c0ff8Tff7b72]Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4) Te6edf3or
Tb4b4b4(Tb4b4b4(Te6edf3dataTff7b72[T79c0ff9Tff7b72]Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4) Te6edf3shl T79c0ff8Tb4b4b4) Te6edf3or
Tb4b4b4(Tb4b4b4(Te6edf3dataTff7b72[T79c0ff1T79c0ff0Tff7b72]Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4) Te6edf3shl T79c0ff1T79c0ff6Tb4b4b4) Te6edf3or
Tb4b4b4(Tb4b4b4(Te6edf3dataTff7b72[T79c0ff1T79c0ff1Tff7b72]Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4) Te6edf3shl T79c0ff2T79c0ff4Tb4b4b4)
Tb4b4b4}
Tff7b72return Te6edf3listOfTb4b4b4(Te6edf3startResponseTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tff7b72if Tb4b4b4(Te6edf3firmwareTransferStartedTb4b4b4) Tb4b4b4{
Te6edf3packetBytesReceived Tff7b72+Tff7b72= Te6edf3dataTb4b4b4.Te6edf3size
Te6edf3packetsSinceLastPrnTff7b72+Tff7b72+
Te6edf3onFirmwarePacketWriteTff7b72?.Te6edf3invokeTb4b4b4(Te6edf3packetBytesReceivedTb4b4b4)
Tff7b72val Te6edf3responses Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3ByteArrayTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3firmwareDone Tff7b72= Te6edf3packetBytesReceived Tff7b72>Tff7b72= Te6edf3expectedFirmwareSize
T8b949e// Use the cadence the transport actually requested via PACKET_RECEIPT_NOTIF_REQ so a RECOVERY
T8b949e// (prnInterval=5) upload receives receipts at the smaller interval.
Tff7b72if Tb4b4b4(Te6edf3packetsSinceLastPrn Tff7b72>Tff7b72= Te6edf3prnInterval Tff7b72&Tff7b72& Tff7b72!Te6edf3firmwareDoneTb4b4b4) Tb4b4b4{
Te6edf3packetsSinceLastPrn Tff7b72= T79c0ff0
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3suppressPrnNotificationsTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3reported Tff7b72=
Tff7b72if Tb4b4b4(Te6edf3scheme Tff7b72=Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3PrnUnderReportTb4b4b4) Tb4b4b4{
Te6edf3packetBytesReceived Tff7b72- T79c0ff1
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3packetBytesReceived
Tb4b4b4}
Te6edf3responses Tff7b72+Tff7b72= Te6edf3packetReceiptTb4b4b4(Te6edf3reportedTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3firmwareDoneTb4b4b4) Tb4b4b4{
Te6edf3responses Tff7b72+Tff7b72= Te6edf3successTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RECEIVE_FIRMWARE_IMAGETb4b4b4)
Tb4b4b4}
Tff7b72return Te6edf3responsesTb4b4b4.Te6edf3takeIf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}
T8b949e// Init-packet writes between START and COMPLETE: silent.
Tff7b72return Tff7b72null
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffstartResponseTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ByteArray Tff7b72= Tff7b72when Tb4b4b4(Te6edf3schemeTb4b4b4) Tb4b4b4{
Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3RejectStart Tff7b72-Tff7b72>
Te6edf3byteArrayOfTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RESPONSE_CODETb4b4b4, Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3START_DFUTb4b4b4, Te6edf3LegacyDfuStatusTb4b4b4.Te6edf3NOT_SUPPORTEDTb4b4b4)

Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3RejectStartInvalidState Tff7b72-Tff7b72>
Te6edf3byteArrayOfTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RESPONSE_CODETb4b4b4, Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3START_DFUTb4b4b4, Te6edf3LegacyDfuStatusTb4b4b4.Te6edf3INVALID_STATETb4b4b4)

Tff7b72else Tff7b72-Tff7b72> Te6edf3successTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3START_DFUTb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffinitResponseTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ByteArray Tff7b72= Tff7b72when Tb4b4b4(Te6edf3schemeTb4b4b4) Tb4b4b4{
Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3RejectInit Tff7b72-Tff7b72>
Te6edf3byteArrayOfTb4b4b4(
Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RESPONSE_CODETb4b4b4,
Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3INIT_DFU_PARAMSTb4b4b4,
Te6edf3LegacyDfuStatusTb4b4b4.Te6edf3OPERATION_FAILEDTb4b4b4,
Tb4b4b4)

Tff7b72else Tff7b72-Tff7b72> Te6edf3successTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3INIT_DFU_PARAMSTb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffvalidateResponseTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ByteArray Tff7b72= Tff7b72when Tb4b4b4(Te6edf3schemeTb4b4b4) Tb4b4b4{
Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3RejectValidate Tff7b72-Tff7b72>
Te6edf3byteArrayOfTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RESPONSE_CODETb4b4b4, Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3VALIDATETb4b4b4, Te6edf3LegacyDfuStatusTb4b4b4.Te6edf3CRC_ERRORTb4b4b4)

Tff7b72else Tff7b72-Tff7b72> Te6edf3successTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3VALIDATETb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffpacketReceiptTb4b4b4(Te6edf3bytesReceivedTb4b4b4: Tffa657LongTb4b4b4)Tb4b4b4: Te6edf3ByteArray Tff7b72= Te6edf3byteArrayOfTb4b4b4(
Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3PACKET_RECEIPTTb4b4b4,
Tb4b4b4(Te6edf3bytesReceived Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4)Tb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4(Tb4b4b4(Te6edf3bytesReceived Te6edf3ushr T79c0ff8Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4)Tb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4(Tb4b4b4(Te6edf3bytesReceived Te6edf3ushr T79c0ff1T79c0ff6Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4)Tb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4(Tb4b4b4(Te6edf3bytesReceived Te6edf3ushr T79c0ff2T79c0ff4Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4)Tb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffsuccessTb4b4b4(Te6edf3opcodeTb4b4b4: Tffa657ByteTb4b4b4)Tb4b4b4: Te6edf3ByteArray Tff7b72=
Te6edf3byteArrayOfTb4b4b4(Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RESPONSE_CODETb4b4b4, Te6edf3opcodeTb4b4b4, Te6edf3LegacyDfuStatusTb4b4b4.Te6edf3SUCCESSTb4b4b4)
Tb4b4b4}

T8b949e/**
* [StateFlow] wrapper that exposes the delegate's [value][StateFlow.value] but filters
* [BleConnectionState.Disconnected] emissions from collectors. Used by the write-throws-on-disconnect race tests:
* the outer stream tripwire's `connectionState.first { Disconnected }` watcher stays suspended (modelling the race
* where the write throws before the watcher processes the emission), while `bleConnection.connectionState.value`
* still reads as Disconnected for the write-catch classification.
*/
Tff7b72private Tff7b72class T56d364DisconnectEmissionsSuppressedStateFlowTb4b4b4(Tff7b72private Tff7b72val Te6edf3delegateTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3BleConnectionStateTff7b72>Tb4b4b4) Tb4b4b4:
Te6edf3StateFlowTff7b72<Te6edf3BleConnectionStateTff7b72> Tb4b4b4{
Tff7b72override Tff7b72val Te6edf3valueTb4b4b4: Te6edf3BleConnectionState
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3value

Tff7b72override Tff7b72val Te6edf3replayCacheTb4b4b4: Te6edf3ListTff7b72<Te6edf3BleConnectionStateTff7b72>
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3replayCache

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffcollectTb4b4b4(Te6edf3collectorTb4b4b4: Te6edf3FlowCollectorTff7b72<Te6edf3BleConnectionStateTff7b72>Tb4b4b4)Tb4b4b4: Te6edf3Nothing Tb4b4b4{
Te6edf3delegateTb4b4b4.Te6edf3collect Tb4b4b4{ Te6edf3state Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Te6edf3state Tff7b72is Te6edf3BleConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4) Tff7b72returnTf0883e@collect
Te6edf3collectorTb4b4b4.Te6edf3emitTb4b4b4(Te6edf3stateTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/** BleConnection wrapper that swaps in the auto-responding service for `profile()` calls. */
Tff7b72private Tff7b72class T56d364AutoRespondingBleConnectionTb4b4b4(
Tff7b72private Tff7b72val Te6edf3delegateTb4b4b4: Te6edf3FakeBleConnectionTb4b4b4,
Tff7b72val Te6edf3autoServiceTb4b4b4: Te6edf3AutoRespondingLegacyServiceTb4b4b4,
Tff7b72private Tff7b72val Te6edf3profileScopeTb4b4b4: Te6edf3CoroutineScopeTb4b4b4,
Te6edf3suppressDisconnectEmissionsTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72falseTb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3BleConnection Tb4b4b4{
Tff7b72override Tff7b72val Te6edf3deviceTb4b4b4: Te6edf3BleDevice?
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3device

Tff7b72override Tff7b72val Te6edf3deviceFlowTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3BleDevice?Tff7b72>
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3deviceFlow

Tff7b72override Tff7b72val Te6edf3connectionStateTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3BleConnectionStateTff7b72> Tff7b72=
Tff7b72if Tb4b4b4(Te6edf3suppressDisconnectEmissionsTb4b4b4) Tb4b4b4{
Te6edf3DisconnectEmissionsSuppressedStateFlowTb4b4b4(Te6edf3delegateTb4b4b4.Te6edf3connectionStateTb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3delegateTb4b4b4.Te6edf3connectionState
Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffconnectTb4b4b4(Te6edf3deviceTb4b4b4: Te6edf3BleDeviceTb4b4b4) Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3connectTb4b4b4(Te6edf3deviceTb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffconnectAndAwaitTb4b4b4(Te6edf3deviceTb4b4b4: Te6edf3BleDeviceTb4b4b4, Te6edf3timeoutTb4b4b4: Te6edf3DurationTb4b4b4) Tff7b72=
Te6edf3delegateTb4b4b4.Te6edf3connectAndAwaitTb4b4b4(Te6edf3deviceTb4b4b4, Te6edf3timeoutTb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffdisconnectTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3delegateTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Tff7b72<Te6edf3TTff7b72> Td2a8ffprofileTb4b4b4(
Te6edf3serviceUuidTb4b4b4: Te6edf3kotlinTb4b4b4.Te6edf3uuidTb4b4b4.Te6edf3UuidTb4b4b4,
Te6edf3timeoutTb4b4b4: Te6edf3DurationTb4b4b4,
Te6edf3setupTb4b4b4: Te6edf3suspend Te6edf3CoroutineScopeTb4b4b4.Tb4b4b4(Te6edf3BleServiceTb4b4b4) Tff7b72-Tff7b72> Te6edf3TTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3T Tff7b72= Te6edf3profileScopeTb4b4b4.Te6edf3setupTb4b4b4(Te6edf3autoServiceTb4b4b4)

Tff7b72override Tff7b72fun Td2a8ffmaximumWriteValueLengthTb4b4b4(Te6edf3writeTypeTb4b4b4: Te6edf3BleWriteTypeTb4b4b4)Tb4b4b4: Tffa657Int? Tff7b72=
Te6edf3delegateTb4b4b4.Te6edf3maximumWriteValueLengthTb4b4b4(Te6edf3writeTypeTb4b4b4)
Tb4b4b4}

T8b949e// -----------------------------------------------------------------------
T8b949e// Stream profile selection
T8b949e// -----------------------------------------------------------------------

Tf0883e@Test
Tff7b72fun Td2a8ff`NORMAL profile writes PRN request value 10`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Te6edf3profile Tff7b72= Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3NORMALTb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff4T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4}

Tff7b72val Te6edf3prnWrite Tff7b72= Te6edf3envTb4b4b4.Te6edf3controlPointWritesTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3single Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTff7b72[T79c0ff0Tff7b72] Tff7b72=Tff7b72= Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3PACKET_RECEIPT_NOTIF_REQ Tb4b4b4}
Tff7b72val Te6edf3requested Tff7b72= Tb4b4b4(Te6edf3prnWriteTb4b4b4.Te6edf3dataTff7b72[T79c0ff1Tff7b72]Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4) Te6edf3or Tb4b4b4(Tb4b4b4(Te6edf3prnWriteTb4b4b4.Te6edf3dataTff7b72[T79c0ff2Tff7b72]Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4) Te6edf3shl T79c0ff8Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3PRN_INTERVAL_PACKETSTb4b4b4, Te6edf3requestedTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`RECOVERY profile writes PRN request value 5`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Te6edf3profile Tff7b72= Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3RECOVERYTb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff4T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4}

Tff7b72val Te6edf3prnWrite Tff7b72= Te6edf3envTb4b4b4.Te6edf3controlPointWritesTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3single Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTff7b72[T79c0ff0Tff7b72] Tff7b72=Tff7b72= Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3PACKET_RECEIPT_NOTIF_REQ Tb4b4b4}
Tff7b72val Te6edf3requested Tff7b72= Tb4b4b4(Te6edf3prnWriteTb4b4b4.Te6edf3dataTff7b72[T79c0ff1Tff7b72]Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4) Te6edf3or Tb4b4b4(Tb4b4b4(Te6edf3prnWriteTb4b4b4.Te6edf3dataTff7b72[T79c0ff2Tff7b72]Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Te6edf3and T79c0ff0Te6edf3xFFTb4b4b4) Te6edf3shl T79c0ff8Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3RECOVERY_PRN_INTERVAL_PACKETSTb4b4b4, Te6edf3requestedTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`RECOVERY profile awaits packet receipts at the smaller interval`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
T8b949e// RECOVERY prnInterval = 5, so a 100-byte firmware at 20-byte packets fires a PRN exactly once at byte 100
T8b949e// (5 packets Γ— 20B) β€” but offset == firmware.size at that point means the `offset < firmware.size` gate
T8b949e// suppresses the wait. Use 110 bytes (would need 6 packets at NORMAL=10 to hit the first PRN) so the
T8b949e// first PRN at 100B fires under RECOVERY but never fires under NORMAL within the 110B upload.
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Te6edf3profile Tff7b72= Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3RECOVERYTb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3progress Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657FloatTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff1T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3progress Tff7b72+Tff7b72= Tffa657it Tb4b4b4}

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6fftransferFirmware failed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
T8b949e// A PRN fires at offset 100 (5 packets Γ— 20B under RECOVERY prnInterval=5); progress must record it during
T8b949e// streaming, then the final 1f at completion. This proves the PRN was consumed mid-stream, not just drained
T8b949e// by the final response wait.
Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(T79c0ff1T79c0ff0T79c0ff0f Tff7b72/ T79c0ff1T79c0ff1T79c0ff0fTb4b4b4, T79c0ff1fTb4b4b4)Tb4b4b4, Te6edf3progressTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`NORMAL profile does not await a PRN for a sub-interval upload`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
T8b949e// 110-byte firmware at 20-byte packets = 6 packets. NORMAL prnInterval = 10, so no PRN is due before the
T8b949e// upload completes β€” the auto-responder must NOT emit a PRN, and the transport must NOT call
T8b949e// awaitPacketReceiptDuringStream.
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Te6edf3profile Tff7b72= Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3NORMALTb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff1T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4}

Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6fftransferFirmware failed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e// -----------------------------------------------------------------------
T8b949e// Mid-stream disconnect
T8b949e// -----------------------------------------------------------------------

Tf0883e@Test
Tff7b72fun Td2a8ff`mid-stream disconnect returns typed MidStreamDisconnect with correct byte counts`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath

T8b949e// Hold the first firmware-packet write in flight so the transfer cannot run through all 200 bytes before
T8b949e// the test injects the disconnect. Production streamOffset has already published 20 before the write, so
T8b949e// the tripwire observer will read bytesSent == 20 when the link drops.
Tff7b72val Te6edf3firstPacketWriteInFlight Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3onFirmwarePacketWrite Tff7b72= Tb4b4b4{ Te6edf3bytes Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Te6edf3bytes Tff7b72>Tff7b72= T79c0ff2T79c0ff0LTb4b4b4) Tb4b4b4{
Te6edf3firstPacketWriteInFlightTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3awaitCancellationTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3transferDeferred Tff7b72= Te6edf3async Tb4b4b4{ Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4} Tb4b4b4}
Te6edf3firstPacketWriteInFlightTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)

Te6edf3assertTrueTb4b4b4(
Tff7b72!Te6edf3transferDeferredTb4b4b4.Te6edf3isCompletedTb4b4b4,
Ta5d6ff"Ta5d6fftransfer must remain suspended inside the first firmware-packet writeTa5d6ff"Tb4b4b4,
Tb4b4b4)

Te6edf3envTb4b4b4.Te6edf3connectionTb4b4b4.Te6edf3simulateRemoteDisconnectTb4b4b4(Tb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3result Tff7b72= Te6edf3transferDeferredTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3error Tff7b72=
Te6edf3assertIsTff7b72<Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3MidStreamDisconnectTff7b72>Tb4b4b4(
Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffexpected the outer stream tripwire to classify the disconnect while the first write was in flightTa5d6ff"Tb4b4b4,
Tb4b4b4)
T8b949e// First packet write was in flight (streamOffset published as 20 before service.write), so bytesSent == 20.
Te6edf3assertEqualsTb4b4b4(T79c0ff2T79c0ff0Tb4b4b4, Te6edf3errorTb4b4b4.Te6edf3bytesSentTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4, Te6edf3errorTb4b4b4.Te6edf3totalBytesTb4b4b4)
T8b949e// No PRN was received before the drop (first PRN would be at byte 200), so lastConfirmedBytes is -1.
Te6edf3assertEqualsTb4b4b4(Tff7b72-T79c0ff1Tb4b4b4, Te6edf3errorTb4b4b4.Te6edf3lastConfirmedBytesTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3BleConnectionStateTb4b4b4.Te6edf3DisconnectedTff7b72>Tb4b4b4(Te6edf3errorTb4b4b4.Te6edf3connectionStateTb4b4b4)
Tb4b4b4}

T8b949e/**
* A drop during a stream PRN wait must be classified by the OUTER stream-level tripwire as
* [LegacyDfuException.MidStreamDisconnect] β€” NOT as a generic handshake-style [DfuException.ConnectionFailed]. The
* stream PRN wait uses [awaitPacketReceiptDuringStream], which deliberately does NOT install its own disconnect
* tripwire, so the outer watcher is the sole classifier.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`stream-level tripwire classifies a drop during a PRN wait as MidStreamDisconnect`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath
T8b949e// Suppress PRNs so the transport suspends inside awaitPacketReceiptDuringStream at the first PRN boundary.
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3suppressPrnNotifications Tff7b72= Tff7b72true

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)

T8b949e// First NORMAL PRN boundary = prnInterval (10) Γ— default packet size (20B, MTU not negotiated) = byte 200.
T8b949e// The responder fires this hook synchronously after accounting the 10th packet write, so completing the
T8b949e// deferred proves the stream has written bytes 0..200 and is about to enter awaitPacketReceiptDuringStream β€”
T8b949e// long before virtual time can reach the 30s COMMAND_TIMEOUT that would otherwise mask the tripwire path.
Tff7b72val Te6edf3reachedPrnBoundary Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3onFirmwarePacketWrite Tff7b72= Tb4b4b4{ Te6edf3bytes Tff7b72-Tff7b72> Tff7b72if Tb4b4b4(Te6edf3bytes Tff7b72>Tff7b72= T79c0ff2T79c0ff0T79c0ff0LTb4b4b4) Te6edf3reachedPrnBoundaryTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4) Tb4b4b4}

T8b949e// Run the transfer asynchronously so the test body can synchronize the disconnect to the suspended PRN wait.
Tff7b72val Te6edf3transferDeferred Tff7b72= Te6edf3async Tb4b4b4{ Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff2T79c0ff2T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4} Tb4b4b4}
T8b949e// Wait for the 10th packet write to land, then flush dispatched continuations so the stream coroutine reaches
T8b949e// the suspended receive() inside awaitPacketReceiptDuringStream β€” without advancing virtual time.
Te6edf3reachedPrnBoundaryTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)

T8b949e// Simulate remote disconnect while the transport is suspended in awaitPacketReceiptDuringStream.
Te6edf3envTb4b4b4.Te6edf3connectionTb4b4b4.Te6edf3simulateRemoteDisconnectTb4b4b4(Tb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3result Tff7b72= Te6edf3transferDeferredTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Tff7b72val Te6edf3ex Tff7b72= Te6edf3assertIsTff7b72<Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3MidStreamDisconnectTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
T8b949e// Must be the typed MidStreamDisconnect from the outer stream-level tripwire β€” NOT the generic
T8b949e// ConnectionFailed that an inner handshake-style tripwire would have produced, and NOT a local Timeout.
Te6edf3assertEqualsTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4, Te6edf3exTb4b4b4.Te6edf3bytesSentTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2T79c0ff2T79c0ff0Tb4b4b4, Te6edf3exTb4b4b4.Te6edf3totalBytesTb4b4b4)
Tb4b4b4}

T8b949e/**
* Race: `service.write()` throws BEFORE the outer stream tripwire's state-flow watcher processes the Disconnected
* emission. The write-catch re-check must classify the disconnect as [LegacyDfuException.MidStreamDisconnect] so
* the retry coordinator can switch to the RECOVERY profile β€” the raw write exception must NOT escape.
*
* [DisconnectEmissionsSuppressedStateFlow] models the race window: the tripwire watcher never sees Disconnected
* (its collector filters it), while `connectionState.value` still reads as Disconnected for the write-catch.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`failed write with disconnected connection classifies as MidStreamDisconnect`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Te6edf3suppressDisconnectEmissions Tff7b72= Tff7b72trueTb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath

Tff7b72val Te6edf3writeFailure Tff7b72= Te6edf3RuntimeExceptionTb4b4b4(Ta5d6ff"Ta5d6ffsimulated write failure on dead linkTa5d6ff"Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3onFirmwarePacketWrite Tff7b72= Tb4b4b4{ Te6edf3bytes Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Te6edf3bytes Tff7b72>Tff7b72= T79c0ff2T79c0ff0LTb4b4b4) Tb4b4b4{
Te6edf3envTb4b4b4.Te6edf3connectionTb4b4b4.Te6edf3simulateRemoteDisconnectTb4b4b4(Tb4b4b4)
Tff7b72throw Te6edf3writeFailure
Tb4b4b4}
Tb4b4b4}

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4}

Tff7b72val Te6edf3error Tff7b72=
Te6edf3assertIsTff7b72<Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3MidStreamDisconnectTff7b72>Tb4b4b4(
Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffexpected the write-catch to classify the disconnect that beat the tripwire watcherTa5d6ff"Tb4b4b4,
Tb4b4b4)
T8b949e// streamOffset was published as 20 before the first firmware-packet write, so bytesSent == 20.
Te6edf3assertEqualsTb4b4b4(T79c0ff2T79c0ff0Tb4b4b4, Te6edf3errorTb4b4b4.Te6edf3bytesSentTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4, Te6edf3errorTb4b4b4.Te6edf3totalBytesTb4b4b4)
T8b949e// No PRN was received before the drop (first PRN would be at byte 200), so lastConfirmedBytes is -1.
Te6edf3assertEqualsTb4b4b4(Tff7b72-T79c0ff1Tb4b4b4, Te6edf3errorTb4b4b4.Te6edf3lastConfirmedBytesTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3BleConnectionStateTb4b4b4.Te6edf3DisconnectedTff7b72>Tb4b4b4(Te6edf3errorTb4b4b4.Te6edf3connectionStateTb4b4b4)
Te6edf3assertSameTb4b4b4(Te6edf3writeFailureTb4b4b4, Te6edf3errorTb4b4b4.Te6edf3causeTb4b4b4)
Tb4b4b4}

T8b949e/**
* When the link is still Connected, a failed write must NOT be reclassified as
* [LegacyDfuException.MidStreamDisconnect] β€” the original exception must escape unchanged so the caller can react
* to the real cause instead of mistaking it for a link drop.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`failed write with active connection is not reclassified`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3scheme Tff7b72= Te6edf3LegacyResponderSchemeTb4b4b4.Te6edf3HappyPath

Tff7b72val Te6edf3writeFailure Tff7b72= Te6edf3RuntimeExceptionTb4b4b4(Ta5d6ff"Ta5d6ffsimulated write failure on live linkTa5d6ff"Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3responderTb4b4b4.Te6edf3onFirmwarePacketWrite Tff7b72= Tb4b4b4{ Te6edf3bytes Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Te6edf3bytes Tff7b72>Tff7b72= T79c0ff2T79c0ff0LTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3writeFailure
Tb4b4b4}
Tb4b4b4}

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferInitPacketTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3transferFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{Tb4b4b4}

T8b949e// The write failure must NOT be classified as MidStreamDisconnect while the connection is active.
T8b949e// assertIs<RuntimeException> proves it was not reclassified; message equality confirms it's the
T8b949e// same failure (coroutine stack-trace recovery may copy the exception across coroutineScope boundaries).
Tff7b72val Te6edf3error Tff7b72=
Te6edf3assertIsTff7b72<Te6edf3RuntimeExceptionTff7b72>Tb4b4b4(
Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffa write failure on an active connection must not become MidStreamDisconnectTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3writeFailureTb4b4b4.Te6edf3messageTb4b4b4, Te6edf3errorTb4b4b4.Te6edf3messageTb4b4b4)
Tb4b4b4}

T8b949e// -----------------------------------------------------------------------
T8b949e// Bounded RESET (abort)
T8b949e// -----------------------------------------------------------------------

Tf0883e@Test
Tff7b72fun Td2a8ff`RESET is written WITH_RESPONSE`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3abortTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3resetWrite Tff7b72= Te6edf3envTb4b4b4.Te6edf3controlPointWritesTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3single Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4) Tff7b72&Tff7b72& Tffa657itTb4b4b4.Te6edf3dataTff7b72[T79c0ff0Tff7b72] Tff7b72=Tff7b72= Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RESET Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITH_RESPONSETb4b4b4, Te6edf3resetWriteTb4b4b4.Te6edf3writeTypeTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`acknowledged RESET completes after response`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)

Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3abortTb4b4b4(Tb4b4b4)

T8b949e// An acknowledged RESET means the write completed within the timeout; abort must return normally.
Tff7b72val Te6edf3resetWrite Tff7b72= Te6edf3envTb4b4b4.Te6edf3controlPointWritesTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3single Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4) Tff7b72&Tff7b72& Tffa657itTb4b4b4.Te6edf3dataTff7b72[T79c0ff0Tff7b72] Tff7b72=Tff7b72= Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RESET Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resetWriteTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3size Tff7b72=Tff7b72= T79c0ff1Tb4b4b4, Ta5d6ff"Ta5d6ffRESET payload is a single opcode byteTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`abort returns within RESET_WRITE_TIMEOUT when the RESET write hangs`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3hangOnControlPointWrites Tff7b72= Tff7b72true

T8b949e// Run abort on the test dispatcher so virtual time advances past RESET_WRITE_TIMEOUT. The hanging write
T8b949e// never completes; withTimeoutOrNull inside abort must return null and abort must return normally.
Tff7b72val Te6edf3abortJob Tff7b72= Te6edf3async Tb4b4b4{ Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3abortTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3abortJobTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)

T8b949e// No RESET write should have been recorded on the delegate β€” the hang prevented the write from reaching
T8b949e// FakeBleService.write, so an "acknowledged" log would be a lie.
Tff7b72val Te6edf3resetWrites Tff7b72=
Te6edf3envTb4b4b4.Te6edf3controlPointWritesTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4) Tff7b72&Tff7b72& Tffa657itTb4b4b4.Te6edf3dataTff7b72[T79c0ff0Tff7b72] Tff7b72=Tff7b72= Te6edf3LegacyDfuOpcodeTb4b4b4.Te6edf3RESET Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resetWritesTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffRESET write should not have been recorded when the link hungTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`abort propagates parent cancellation rather than reporting success`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3hangOnControlPointWrites Tff7b72= Tff7b72true

T8b949e// parentJob stands in for the caller's scope; cancelling it must propagate through withTimeoutOrNull
T8b949e// (which only swallows its own TimeoutCancellationException, not parent cancellation) and out of abort.
Tff7b72val Te6edf3parentJob Tff7b72= Te6edf3JobTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3abortDeferred Tff7b72= Te6edf3asyncTb4b4b4(Te6edf3parentJobTb4b4b4) Tb4b4b4{ Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3abortTb4b4b4(Tb4b4b4) Tb4b4b4}

T8b949e// Let abort reach the hanging RESET write.
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3parentJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3assertFailsWithTff7b72<Te6edf3CancellationExceptionTff7b72> Tb4b4b4{ Te6edf3abortDeferredTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`abort does not swallow Error subtypes from the RESET write`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3throwErrorOnControlPointWrites Tff7b72= Tff7b72true

T8b949e// abort catches Exception (operational link failures) but lets Error subtypes propagate.
Te6edf3assertFailsWithTff7b72<Te6edf3AssertionErrorTff7b72> Tb4b4b4{ Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3abortTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`abort swallows operational Exception from the RESET write`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3env Tff7b72= Te6edf3createConnectedTransportTb4b4b4(Tb4b4b4)
Te6edf3envTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3throwExceptionOnControlPointWrites Tff7b72= Tff7b72true

T8b949e// A RuntimeException from the control-point write is an operational Exception β€” abort must catch it
T8b949e// (best-effort, non-fatal) and return normally without rethrowing.
Te6edf3envTb4b4b4.Te6edf3transportTb4b4b4.Te6edf3abortTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.28s